/**
 * 先来对 indexed DB 做一个整体上的概括，包括基本概念、基本流程等。
 * 
 * DataBase——数据库：
 *    每一个“源”（协议+域名+端口）都关联一组数据库。
 *    每一个数据库都有一个或多个“对象仓库”（object store）用于存储数据。
 *    每个数据库都有一个名字（name）用于在同一源下标识自身。这个名字一旦确定就不会改变。
 *    每个数据库都有一个版本（version），数据库创建时默认为 0。某一时刻一个数据库只能存在一个版本，唯一改变版本的方法是使用“升级事务”（upgrade transaction）
 *    一个数据库至多只能关联一个升级事务。
 * 
 * connection——链接：
 *    脚本不能世界和数据库交互，而是要通过 "链接"（connection）间接访问。
 *    链接对象可用于操作数据库，也是唯一获取对应数据库 “事务”（transaction）的方法。
 *    打开数据库的行为会创建一个链接，某一个时刻可能有多个链接关联到该数据库。
 *    一个链接只能访问到当前全局作用域的源下的数据库，且该数据库必须处于打开状态。
 *    链接也有版本，在创建时被设置。版本状态在整个链接的存活时间中保持不变，除非升级行为中断，这时版本状态被设置城数据库的上一个版本，一旦链接关闭，版本就不再改变。
 *    链接初始化时是 opened 状态，这种状态下链接可以被 closed，如果创建链接的执行上下文被销毁，链接会关闭。也可以明确调用方法关闭数据库链接，当链接关闭时， close pending flag 被设置。
 *    UA 有时会因为发现异常而关闭链接，比如因访问文件系统失败、权限变化或者清除了当前源下的仓库。
 *    一个链接有一个“对象仓库集合”（object store set），这个集合在链接创建时根据关联的数据库的对象仓库进行初始化。这个集合内容会维持不变，除非执行升级事务。
 *    在一个已经打开的链接上，如果试图去升级或者删除数据库，会触发一个“版本改变事件”（version change event）。可在该事件句柄里关闭链接，一遍升级或删除继续进行。
 * 
 * Object Store——对象仓库：
 *    "对象仓库"（object store）是数据库的主要存储机制，新创建的数据不包含任何对象参数。
 *    每一个数据库都有一个对象仓库集合，这个集合可以在升级事务抛出的 upgradeneeded 事件的监听中修改。
 *    每个对象仓库有一个“记录列表”（list of records）用来保存数据，每一条”记录“（records）又由“键”（key）和“值”（value）组成。这个列表根据键进行升序排序，并且键永远唯一。
 *    每个对象仓库有一个“名字”（name），在同一数据库下这个名字唯一。
 *    每个对象仓库创建时能可选的给入一个“键路径”（key path），如果有个剑建路径，就说使用了“内部键”（in-line key），否则就是“外部键”（out-in-line key）。
 *    每个对象仓库可选地有一个 "键生成器"（key generator）
 *    每个对象仓库有下面三种方法为记录生成键：
 *        1.通过键生成器。每次使用键生成器会生成一个单调递增的数字
 *        2.通过键路径获取
 *        3.当存入值时可以明确指定
 *    脚本不能直接访问对象仓库，但在事务中可以通过“对象仓库句柄”（object store handle）间接访问
 *    一个对象仓库句柄有一个关联的对象仓库和一个关联的事务。多个句柄可能在不同事务中关联同一个对象仓库，但同一个事务只能有一个对象仓库句柄关联到特定对象仓库。
 *    当对象仓库句柄创建时，会初始化一个“索引集合”（index set），来引用关联的对象仓库。索引仓库的内容保持不变，直到升级事务执行。
 *    对象仓库句柄有一个名字，初始化为关联的对象仓库的名字，名字字段会保持不变，直到升级事务执行。
 * 
 * Value——值：
 *    每一条记录都关联一个“值”（value），用户代理必须支持所有可串行化的对象作为值。
 *    String、Date、Object、Array等实例，File、Blob、ImageData等等都可以
 *    记录的值是值存取而不是引用存取。
 *    值是记录经过 StructuredSerializeForStorage 操作后的输出。
 * 
 * key——键：
 *    为了有效查找记录，indexed DB 中的记录是通过他的"键“（key）来组织的。
 *    每一个键都关联一个“类型”（type)，这个类型是 number、string、date、string、binary、array 中的一种
 *    每一个键都关联一个值，值的类型与键的类型一一队形：number/date: double，string：DOMString、binary：字节序列、array：list
 *    一个“数组键”（array key）是一个数组类型的键，其“子键”（subkeys）对应每个成员值的类型。
 * 
 * Key Path——键路径
 *    “建路径”（key path）是一个字符串或者字符串数组，定义了怎样从值中确定键。一个可用的键路径应该是下面几种：
 *        1.空字符串
 *        2.一个符合js标准的标识符名
 *        3.由周期字符分隔的字符串
 *        4.符合上面几点要求的字符串数组
 *    键路径的值只能在 StructuredSerializeForStorage 拷贝中通过明确属性进行访问，以及下面特定类型的特定属性：
 *        Blob: size、type
 *        File：name、lastModified、lastModifiedDate
 *        Array：length
 *        String：length
 * 
 * Index——索引：
 *    有时使用除了键之外的方式检索记录也很有用，“索引”（index）就允许使用值的某个属性进行检索。
 *    索引本身就是一个特殊的持久化的键值存储，有一个引用的对象仓库，也有一个用于保持数据的记录列表。
 *    只要对象仓库中插入、更新和删除记录，索引中的记录就会自动填充。
 *    索引中记录的值，始终是相关对象仓库的键的值，这里的键就是该对象仓库中使用了键路径指定的那个。
 *    如果索引的对象仓库中键 X 对应值 A，且索引从 A 中通过键路径提取出的值是 Y，那么索引记录中键值分别是：Y：X，其中 X 又是对象仓库记录的键，引用其对应的值
 *    索引中的记录总是根据记录的键进行存储。但与对象仓库不同，索引中可以有多条具有相同键的记录，这样的记录根据索引的记录的值尽心额外存储。
 *    索引有一个名字，在关联的对象仓库中唯一。
 *    索引有一个“唯一标识符”（unique flag），当这个标识被设置，强制索引中不能有相同键。
 *    索引有一个“多实体标识符”（multiEntry flag），这个标识影响当索引键为数组键时索引的行为。如果不设置，索引记录插入一条索引键就是数组键的记录，如果设置 true，为数组键的每个子键插入一条索引记录。
 *    脚本不能直接访问索引，而是要通过“索引句柄”（index handle）间接访问。
 *    一个索引句柄关联一个索引和一个对象仓库句柄。一个索引句柄的事务，是它关联的对象仓库句柄。
 *    在不同事务中，多个句柄关联可以关联同一个索引，但在同一事务中只能有一个索引句柄关联到指定的索引。
 * 
 * Transactions——事务：
 *    “事务”（transactions）被用来在数据库中与数据交互，不论数据是读是写都是通过事务完成的。（原子性）
 *    事务可以在应用或者系统发生错误时提供保护（原子性）。往往被用于存储多条记录或者有条件的修改记录，表现了原子性和耐用性。
 *    事务是连接创建的，拥有一个”作用域“（scope），决定了事务可以和那个对象仓库交互。
 *    事务有一个“模式”（mode），决定了该事务上可以执行那类型的交互，可选值：
 *        “readonly”： 该事务只允许读取数据。（多个可并行）
 *        “readwrite”： 该事务允许从已存在的对象仓库中读取、修改和删除数据。但存储对象和索引不能添加或删除。（无法并行）
 *        “versionchange”：与 readwrite 一样，另外允许对象仓库和索引的增删。（不能并行，不能手动调动，只能在 upgradenedded 事件句柄中）
 *    事务有一个“有效标识”（active flag），决定是否为事务新建一个“请求”（request）。
 *    有效标识一旦被设置，我们就说该事务有效。
 *    事务有一个“清除事件循环”（cleanup event loop），它是一个事件循环。
 *    事务有一个“请求列表”（request list），用于保存产生的请求。
 *    事务有一个“错误对象”（error），当事务被终止时设置。
 *    事务的“获取父级”（get the parent）算法，返回事务的连接。
 *    有一个比较特殊的事务：“升级事务”（upgrade transactions），它的模式为“versionchange”。
 *    当创建连接时如果给入的版本号大于当前的版本号，那么在 upgradeneeded 事件句柄中会自动执行该升级事务
 * 
 * Request——请求：
 *    数据库上的每一个异步操作都是通过“请求”（request）完成的，每个请求就代表了一个操作。
 *    每个请求都有一个“完成标识”（donw flag），初始化时未设置
 *    每个请求都有一个“来源对象”（source object）
 *    每个请求都有一个”结果“（result）和”错误“（error），当完成标识被设置时进行初始化。
 *    每个请求都有一个事务，当事务发出请求时被设置。
 *    当请求完成时设置完成标识，并触发 success 事件，如果在操作期间发生异常，error 被设置，并触发 error 事件。
 *    请求的 get the parent 算法返回该请求的事务。
 *    “打开请求”（open request）是打开连接或者删除数据库时用到的特殊请求，除了 success 和 error 事件外，还会触发 blocked 和 upgradeneeded 事件用于指示进度。
 *    打开请求的来源对象始终为null，并且除非再 upgradeneeded 事件中否则开放请求的事务始终为 null，自然开放请求的 get the parent 算法返回 null。
 * 
 * Key Range——键范围：
 *    可以通过键或者“键范围”（key range）从对象仓库或者索引中获取记录。键范围是键的某些数据类型的连续间隔。
 *    一个键范围有一个关联的“下边界”（lower bound）为 null 或者是一个键。
 *    一个键范围有一个关联的“上边界”（upper bound）为 null 或者是一个键
 *    一个键范围有一个关联的“下开放标识”（lower open flag），除非另有说明否则未设置
 *    一个键范围有一个关联的“上开放标识”（upper open flag），除非另有说明否则未设置
 *    键范围的下边界一定小于等于它的上边界，只包含一个键的键范围的下边界和上边界都等于该键
 *    如果一个键满足下面两个条件，则说明该键在这个键范围中：
 *        1.键范围的下边界为 null，或者小于该键，又或者等于该键且下边界标识未设置
 *        2.键范围的上边界为 null，或者大于该键，又或者等于该键且上边界标识未设置
 *    这里边界标识与键的关系如下：
 *        如果下边界标识未设置，那么下边界键包含在键范围中
 *        如果下边界标识已设置，那么下边界键不包含在键范围中
 *        上边界的情形同理。
 *    如果一个键范围的上下边界都为 null，则这个键范围是一个“无限键范围”（unbounded key range），所有键都包含在其中。
 * 
 * Cursor——游标：
 *    “游标”（cursor）用来在索引或者对象仓库中按指定方向迭代一组记录。
 *    游标有一个事务，当游标被创建时这个事务是有效的。游标有一个记录的范围，既可以在索引中也可以在对象仓库中。
 *    游标有一个源，表明当前迭代的记录来自哪个索引或者对象仓库。
 *    游标有一个“方向”（direction），决定了根据记录键迭代时是递增顺序还是递减顺序，以及在迭代索引时是否跳过重复值。同时也决定了游标的其实位置是在源的开头还是尾部，可用方向值：
 *        “next”：初始位置在源的头部，根据键按照单调递增的顺序，遍历所有记录，包括重复值。
 *        “nextunique”：与 next 类似，但加入了值去重逻辑，如果多个记录具有不同键和相同的值，那么只抛出第一条记录，如果源本身设置了 unique flag，那么行为与 next 一样。
 *        “prev"：初始位置在源的尾部，根据键按照单调递减的顺序，遍历所有记录，包括重复值。
 *        “prevunique“：与 prev 类似，但加入了值去重逻辑。和 nextunique 类似，如果源本身有 unique flag，那么行为与 prev 一样。
 *    游标在迭代记录时会维护一个“定位”（position），这个定位保存了上一个返回的记录的键，在迭代索引时，还引入了“对象仓库定位”（object store position），用于在对象仓库中定位上一条索引记录的值，与本身的定位合用来确定下一个记录。
 *    游标有一个键和一个值，保存最近迭代的记录的键值，并且有一个”已获取值标识“（got value flag）标识是否已设置游标的值，以及是否准备好迭代下一个记录。
 *    如果游标的源是一个对象仓库，那么它的“有效对象仓库”（effective object store）就是它的源，它的“有效键”（effective key）就是它的定位，
 *    如果游标的源是一个索引，那么它的有效对象仓库就是该索引引用的对象仓库，它的有效键是它自己的对象仓库定位。
 * 
 * Key Generators——键生成器：
 *    当创建对象仓库时可以指定一个“键生成器”（key generators），用于为插入的记录生成键。
 *    键生成器内置一个“当前数”（current number），这个当前数量是一个正整数，取值范围是[1, 2^53 + 1]，创建时初始值为1，单调递增地作为记录的默认键。（行为与关系型数据库的默认主键id非常类似）
 *    当存储一条记录时指定一个键，关联的键生成器也会更新（current number）
 *    不同对象仓库的当前数相互独立互不影响，如果插入记录失败当前数不会增加，删除甚至清空条目都不会影响当前数。
 *    唯一能影响键生成器的方式是在插入记录时明确给出一个数字类型的键，那么下次插入默认会设置键为大于上一个键的最小值。
 *    终止一个事务会回退该事务中增加的所有键。
 *    如果在创建对象仓库时给出的 key path 在真实存储值中不存在，那么系统会创建这个路径，并将键生成器的值付给它。
 */

// 上文一百来行文字洋洋洒洒，主要介绍了 indexed DB 涉及到的基本概念及其特性，但现在还未开始学习 API
// indexed DB API 是一套非常复杂的 API，用于在客户端存储大量数据，以服务于离线模式（常见应用场景）
// 现阶段线上服务中使用数据库的场景还较少，但随着应用逐渐变大，特别是spa应用，indexed DB 会有越来越宽的应用场景

// 下面我们看看 indexedDB 的核心 API
/**
 * IDBRequest 接口：提供方法访问数据库异步请求结果，以及数据库对象使用事件句柄 IDL 属性。
 *    本接口的实例，来自于各种创建异步请求的方法的返回值
 *    IDBRequest 字典： {
 *        result: {any}，只读，当”donw flag“未设置时抛出一个异常，返回error时 result 的 getter 返回 undefined，否色返回 result。
 *        error: {DOMException}，只读，请求未完成是返回抛出一个错误，否则 error 的 getter 返回 error 字段，或者没有错误发生时返回 null。
 *        source: {IDBObjectStore|IDBIndex|IDBCursor}，只读，请求的源，可以是对象仓库、索引或者游标，如果当前请求是打开请求则为 null。
 *        transaction：{IDBTransaction}，只读，创建请求的事务，如果是打开请求则返回升级事务，否则返回 null
 *        readyState: {IDBRequestReadyState}，只读，枚举类型，一直返回 "pending" 直到请求完成则返回 "doen"
 *        onsuccess: {EventHandler}，成功事件
 *        onerror：{EventHandler}，失败事件
 *        除了上面两个事件外，如果当前请求是一个打开请求，那么还有 onblocked 和 onupgradeneeded 两个进度事件
 *    }
 *    enum IDBRequestReadyState 字典：{ "pending": "done": }
 * 
 * IDBFactory 接口：通过本接口实例上的方法可以访问数据库对象。在支持 IDB 的环境下，往往全局作用域中有一个对象实现了这个几口（web 上是 window.indexedDB）
 *    IDBFactory 字典： {
 *        open: {Function(string name[, unsigned long long version])}，返回一个打开请求，以指定版本打开指定数据库，如果不存在则创建，如果成功则返回的请求对象的 result 属性就是连接对象
 *        deleteDatabase: {Function(string name)}，返回一个打卡请求，试图删除指定数据库，如果试图删除一个有连接的数据库，那么会触发一个 versionchange 事件（在这里调用关闭），请求会被阻塞直到请求关闭。
 *    }
 * 
 * IDBDatabase 接口：本接口实例代表了一个数据库的连接。如果该连接的 close pending flag 标识未被设置，则实例不会被垃圾回收，如果被回收那连接一定关闭。
 *    IDBDatabase 字典： {
 *        name: {string}，只读，数据库名
 *        version: {unsigned long long}，只读，数据库版本
 *        objectStoreName: {Array<string>}，只读，返回当前数据库中所有对象仓库名列表
 *        transaction: {Function(string|Array<string> storeNames[, IDBTransactionMode mode ="readonly"])}，创建一个事务，并指定事务类型和其作用域下包含的对象仓库列表
 *        close: {Function}，执行后开始等待所有事务完成，一旦所有事务完成，立即关闭连接。
 *        createObjectStore: {Function(string name[, IDBObjectStoreParameters options])}，使用给定的名字和配置创建一个新的对象仓库并返回，如果没有在升级事务中调用则抛出一个异常。
 *        deleteObjectStore: {Function(string name)}，删除指定名字的对象仓库，同样，如果没有在升级事务中调用则抛出一个异常。
 *        下面是四个事件：
 *        onabort: 中断事件，onclose: 关闭事件，onerror: 错误事件，onversionchange: 版本变化事件
 *    }
 *    IDBObjectStoreParameters 字典： {
 *        keyPath: {string|Array<string>}，键路径，指定记录的值中某一个属性为键
 *        autoIncrement: {boolean}，是否启用键生成器，单调递增生成键
 *    }
 * 
 * IDBObjectStore 接口：代表了一个对象仓库处理器，对象仓库可以被类似地看做是关系型数据库中的表，它定义的方法提供了操作表中数据的能力。
 *    IDBObjectStore 字典： {
 *        name: {string}，getter 返回仓库的名字，setter 更新仓库的名字（只能在升级事务中进行）
 *        keyPath: {any}，只读，返回仓库看的键路径
 *        indexNames: {Array<string>}，只读，返回仓库所有索引的名字列表
 *        transaction: {IDBTransaction}，只读，返回仓库关联的事务对象
 *        autoIncrement: {boolean}，只读，返回仓库的自增标识
 *        // 下面的几个方法和属性与索引相关
 *        index: {Function(string)}，返回一个 IDBIndex 对象，按照指定名字。
 *        createIndex: {Function(string name, string|Array<string> keyPath, IDBIndexParameters options)}，按照指定名字、建路径和配置创建一个新的索引，如果建路径和配置不能满足已存在的数据，那么会中断 并抛出异常。同时如果没有在升级事务中调用也会抛出异常。
 *        deleteIndex: {Function(string name)}，删除指定名字的索引，若没有在升级事务中调用则抛出异常。
 *        // 下面的方法与游标有关，也返回一个 IDBRequest 对象
 *        openCursor: {Function([any query ,[ IDBCursorDirection direction="next"]])}，在所有匹配 query 的记录上打开一个游标，按照 direction 排序，如果 query 为空，则匹配所有记录。如果成功请求的 result 为带值游标对象，定位在第一条匹配的记录上。如果没有撇皮 result 为 null。
 *        openKeyCursor: {Function([any query, [ IDBCousorDirection direction="next"]])}，在所有匹配 query 的记录上打开一个游标同时按照 key 去重，按照 direction 排序，如果 query 为空则匹配所有记录。如果成功请求的 reuslt 为游标对象，定位在第一条匹配的记录上。如果没有撇皮 result 为 null。
 *        // 下面所有方法如果在一个还未生效的事务中调用会抛出异常。返回值都是 IDBRequest 类型的实例。
 *        get: {Function(any query)}，检索第一个配置给出键或键范围的记录，并获取值。如果成功请求对象的 result 字段为该值，没有匹配到则为 undefined，同时如果键存在但值就为 undefined，则无法区分这两种情况，此时可以使用 openCursor 方法，对这个键或键范围创建游标，如果记录存在则游标值为 undefined，如果记录不存在则有笔不存在。
 *        getKey: {Function(any query)}，索第一个配置给出键或键范围的记录，并获取键。如果成功请求对象的 result 字段为该键，没有匹配到则为 undefined。
 *        getAll: {Function(any query , [强制范围 unsigned long count])}，检索所有匹配给出键或键范围的记录的值，如果给出 count 则最多获取 count 个记录。成功后请求对象的 result 字段为值组成的数组。
 *        getAllKeys: {Function(any query, [强制范围 unsigned long count])}，检索所有匹配给出键或键范围的记录的键，如果给出 count 则最多获取 count 个记录。成功后请求对象的 result 字段为键组成的数组。
 *        count: {Function(any query)}，检索所有匹配给出的键或键范围的记录，统计数量。如果成功请求的 result 为这个数量。
 *        // 下面的方法在"readonly"类型的事务中调用会抛出异常
 *        put: {Function(any value [, any key])}，add: {Function(any value [, any key])}:
 *            两个方法都用于使用指定键和值添加或者更新一条记录。如果仓库使用内部键则会抛出异常。使用 put 时，如果指定的键已存在，那么会替换那条记录。使用 add 时如果键已存在会通过 error 事件抛出异常，使得请求失败。如果成功请求的 result 字段为那条记录的键。
 *        deleete: {Function(any query)}，按照给入的键或者这键范围删除记录。如果成功，请求的 result 字段为 undefined。
 *        clear: {Function}，删除仓库中所有记录
 *    }
 *    创建索引时可选配置参数 IDBIndexParameters 字典： {
 *        unique: {boolean}，索引键是否唯一
 *        multiEntry: {boolean}，多实体
 *    }
 * 
 * IDBIndex 接口：表示一个索引对象实例
 *    IDBIndex 字典：{
 *        name: {string}，返回当前索引的名字，可以在升级事务中赋值修改索引名。
 *        objectStore: {IDBObjectStore}，只读，返回当前索引所属的对象仓库。
 *        keyPath: {any}，只读，返回当前索引的键路径
 *        multiEntry: {Boolean}，只读，返回当前索引的多实例标识值
 *        unique: {Boolean}，只读，返回当前索引的唯一标识值
 *        // 下面这些方法与 IDBObjectStore 中的同名方法类似，行为基本一致，不同之处在于对索引起效而不是对象仓库
 *        get、getKey、getAll、getAllKey、count、openCursor、openKeyCursor
 *    }
 * 
 * IDBKeyRange 接口：每个实例标识一个键范围对象
 *    IDBKeyRange 字典：{
 *        lower: {any}，只读，下边界值
 *        upper: {any}，只读，上边界值
 *        lowerOpen: {boolean}，只读，下边界开放标识（形式与数学中范围的开、闭区间类型）
 *        upperOpen: {boolean}，只读，上边界开放标识
 *        _includes: {Function(any key)}，如果 key 包含在当前键范围中，则返回 true，否则返回 false
 *        // 下面是四个静态方法
 *        only: {Function(any value)}，返回一个 IDBKeyRange，该键范围只包含给出的键
 *        lowerBound: {Function(any key [, open=false])}，返回一个 IDBKeyRange，该建范围已给定键开始，没有上边界键，open 用于指定开放标识
 *        upperBound: {Function(any key [, open=false])}，返回一个 IDBKeyRange，该键范围已给定键结束，没有下边界键，open 用于指定开放标识
 *        bound: {Function(any lower, any upper [, boolean lowerOpen=false [, boolean upperOpen=false]])}，类似的，返回一个 IDBKeyRange ，并直接指定上下边界，及其开放标识
 *    }
 * 
 * IDBCursor 接口：每个实例表示一个游标对象
 *    IDBCursor 字典：{
 *        source: {IDBObjectStore|IDBIndex}，只读，当前游标的源，可以是一个对象仓库或者索引。
 *        direction: {IDBCursorDirection}，只读，当前游标的方向标识
 *        key: {any}，只读，返回当前游标的键（最近一次迭代存储的记录的键）
 *        primaryKey: {any}，只读，返回当前游标的有效键
 *        advance: {Function(unsigned long count)}，让游标在范围内前进 count 条记录，
 *        continue: {Function}，让游标前进一条记录。
 *        continue: {Function(any key)}，让游标在范围内前进到下一条匹配 key 的记录，或者之后
 *        continuePrimaryKey: {Function(any key, any primaryKey)}，只有当源是索引时可用，让游标前进到下一条匹配 key 和 primaryKey 的记录处。
 *        // 下面的两个方法不能在 readonly 的事务中使用，否则会异常，也不能在未生效的事务中使用
 *        update: {Function(any value)}，使用一个新值来更新当前游标指定的记录，返回一个请求，如果成功请求的 result 被设置为记录的键
 *        delete: {Function}，删除当前游标指向的记录，返回一个请求，如果成功请求的 result 为 undefined。
 *    }
 *    如果一个游标没有设置 key only flag，那它就可以实现 IDBCursorWithValue 接口，该接口继承自 IDBCorsur，多一个 value: {any}，只读游标的当前值。
 *    enum IDBCursorDirection 字典： {
 *        "next", “nextunique", "prev", prevunique"
 *    }
 * 
 * IDBTransaction 接口：表示一个事务实例
 *    IDBTransaction 字典：{
 *        objectStoreNames: {Array<string>}，只读，当前事务作用域下关联的所有对象仓库名列表
 *        mode: {IDBTransactionMode}，只读，当前事务类型
 *        db: {IDBDatabase}，只读，当前事务关联的数据库连接对象
 *        error: {DOMException}，只读，异常对象
 *        objectStore: {Function(string name)}，返回一个自定名字的 IDBObjectStore 对象、
 *        abore: {Function}，终止当前事务
 *        // 下面是三个事件句柄
 *        onabort、oncomplete、onerror
 *    }
 */

// 下面封装很多 indexed DB 使用示例
/**
 * Example 1
 * 在这个例子中我们打开了一个数据库连接，创建了数据库 library，并在 library 下创建了对象仓库 books。
 * 还创建了两个索引 titleIdex 和 authorIndex，同时插入了三条记录。
 * 在 onsuccess 事件中通过事务获取存储对象，并调用 get 方法异步的获取到了指定键下的数据值。
 */
const exampleOne = (version) => {
  let request = indexedDB.open('library', version); // 连接 或 创建一个数据库，名为 library，默认版本库为2
  
  // request 是打开请求，因此有 upgradeneeded 事件句柄，该句柄内部就是升级事务本身
  // 所有对数据库本身的修改，包括增删改对象仓库、增删改索引都要在升级事务中进行，且根据 open 时给出的版本信息进行细化
  // 我们第一次运行、或者请求明确请求一个大于当前版本的版本号，才会触发 upgradeneeded 事件，可以在其事件对象上拿到当前上一个版本号
  request.onupgradeneeded = (event) => {
    console.log(`进入升级事务，当前版本：${event.oldVersion}`);
    let db = request.result; // result 就是数据库对象或者叫做连接对象
    let objS_books;
    if(!db.objectStoreNames.contains('books')) { // 判断是否存在并创建
      objS_books = db.createObjectStore('books', {
        keyPath: 'isbn', // isbn：国际标准图书编号
      }); // 指定要存储的对象中 isbn 属性为记录的键
    }
    let titleIndex = objS_books.createIndex('index_by_title', 'title', {
      unique: true
    }); // 创建了一个名为 index_by_title 的索引，指定索引键 title 属性的值，并要求唯一
    let authorIndex = objS_books.createIndex('inde_by_author', 'author');
    // 添加记录
    objS_books.put({ title: 'Quarry Memories', author: 'Fred', isbn: 123456 });
    objS_books.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 });
    objS_books.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 });
    console.log(`升级事务完成，当前版本：${db.version}`);
    event.preventDefault();
  }

  // 监听 request 成功事件，在内部开始数据操作，此时 upgradeneeded 事件已经执行
  request.onsuccess = () => {
    let db = request.result;
    // 所有数据库操作都必须在事务中
    let tx = db.transaction(['books'], 'readonly');
    let objS_books = tx.objectStore('books');
    let get_request = objS_books.get(123456);
    get_request.onsuccess = () => {
      let res = get_request.result;
      console.log(res);
    }
    get_request.onerror = () => {
      console.error(get_request.error);
    }
  }
}

/**
 * Example 2
 * 本例中，我们将升级数据库版本，为 books 对象仓库新创建一个索引by year，并插入两条新数据
 * 让后通过这一索引，查询所有数据。再按照另外索引查询，遍历结果为没有year字段的记录新增字段
 */
const exampleTwo = (version) => {
  let open_request = indexedDB.open('library', version);

  open_request.onupgradeneeded = (event) => {
    console.log(`进入升级事务，当前版本：${event.oldVersion}`);
    let db = open_request.result;
    if(event.oldVersion < version) {
      // 这里的 transaction 是升级事务只能用在这里
      let objS_books = open_request.transaction.objectStore('books');
      let yearIndex = objS_books.createIndex('index_by_year', 'year');
      objS_books.put({ title: 'JavaScript 权威指南', author: 'Flanagan', isbn: 654978, year: 2008 });
      objS_books.put({ title: "CSS 权威指南", author: "Eric A.Meyer", isbn: 542168, year: 2010 });
    }
    console.log(`升级事务完成，当前版本：${db.version}`);
    event.preventDefault();
  }

  open_request.onsuccess = () => {
    let tx = open_request.result.transaction(['books'], 'readwrite');
    let objS_books = tx.objectStore('books');
    let yearIndex = objS_books.index('index_by_year');
    let index_by_year_req = yearIndex.getAll();
    let titleIdex = objS_books.index('index_by_title');
    let index_by_title_req = titleIdex.getAll();

    // 两个查询请求可以并行，但如果涉及到删改则不能并行处理
    index_by_title_req.onsuccess = () => {
      for(let i = 0; i < index_by_title_req.result.length; i += 1) {
        let temp = index_by_title_req.result[i];
        if(!temp.year) {
          let put_req = objS_books.put(Object.assign({}, temp, {
            year: (new Date()).getFullYear()
          }));
          put_req.onsuccess = () => {
            console.log(`修改成功，${put_req.result}`);
          }
          put_req.onerror = () => {
            console.error(put_req.error);
          }
        }
      }
    }
    index_by_title_req.error = () => {
      console.error(index_by_title_req.error);
    }

    index_by_year_req.onsuccess = () => {
      console.log(index_by_year_req.result);
    }
    index_by_year_req.onerror = () => {
      console.error(index_by_year_req.error);
    }
  }
}

/**
 * Example 3
 * 本示例十分简单，主要是看看游标的使用方式
 */
const exampleThree = () => {
  let open_request = indexedDB.open('library');

  open_request.onsuccess = () => {
    let tx = open_request.result.transaction(['books'], 'readwrite');
    let objS_books = tx.objectStore('books');
    let index_by_year = objS_books.index('index_by_year');
    let cursor_req = index_by_year.openCursor();
    
    cursor_req.onsuccess = () => {
      let cursor = cursor_req.result;
      if(cursor && cursor.value.year === 2019) {
        let cursor_update_req = cursor.update(Object.assign({}, cursor.value, {
          year: 2018
        }));
        cursor_update_req.onsuccess = () => {
          console.log('修改成功');
          cursor.continue();
        }
      } else {
        if(cursor) cursor.continue();
      }
    }
  }
}

// exampleOne(1);
// exampleTwo(2);
exampleThree();

/**
 * 到这里 indexedDB 的基本用法已经学习完毕，正如编写上面的代码所感受到的，
 * 大量的基于事件的异步代码让代码结构变得松散而晦涩，对 indexedDB API 进行封装是十分必要的
 * 封装的同时还可以处理鲁棒性，比如 open_request 对象上的 onblocked 事件等等。
 */